//package com.jyf.redis.redisinaction;
//
//import com.google.gson.Gson;
//import redis.clients.jedis.Jedis;
//import redis.clients.jedis.Tuple;
//
//import java.net.MalformedURLException;
//import java.net.URL;
//import java.util.*;
//
//public class Chapter02 {
//    public static final void main(String[] args)
//        throws InterruptedException
//    {
//        new Chapter02().run();
//    }
//
//    public void run()
//        throws InterruptedException
//    {
//        Jedis conn = new Jedis("localhost");
//        conn.select(15);
//        /**
//         * 登录和cookie缓存
//         */
//        testLoginCookies(conn);
//        /**
//         * 使用redis实现购物车
//         */
//        testShopppingCartCookies(conn);
//        /**
//         * 数据库行缓存
//         */
//        testCacheRows(conn);
//        /**
//         * 网页请求缓存
//         */
//        testCacheRequest(conn);
//    }
//
//    /*
//      采用token时，用户信息是用string还是hash存取?
//      方案一：我目前的方案
//          >做缓存时(有过期时间)，用string存储，key为token，value为userJson
//          >做存储时，用hash存储，key为userId，value为userJson
//      方案二：redis实战采用的方案
//          用hash存储有过期时间的用户信息。这样虽然减少了redis中key的数量，但因为token做为key且是随机的，造成hash表中存在的过期数据无法自动删除？
//          redis实战中解决这个问题的方案：
//          用hash存储用户信息，token为key，value为userJson，同时还有一个sorted-set，元素为token，分数为最后一次访问的时间戳。
//          启动一个定时任务，去检查hash表sorted-set的长度是否超过了设定的数量上限，若超过了设定的数量上限，则根据sorted-set中元素的最后访问时间戳升序排序，
//          删除一定数量的最久没访问的token元素，同时根据删除的token，删除hash表中的token键值对。
//      方案二优化：
//          方案二的解决方案，只是做了一个数量上限限制，虽然保证了不会超过内存上限，但如果上限数量设置的不恰当，再超过上限之前仍然会有很多已经过期的数据。
//          我们可以结合redis的过期数据的删除策略和未过期数据的逐出策略，对方案二做进一步优化。可以看出来方案二类似redis的未过期数据的逐出策略，我们再将过期数据的删除策略给加上就好了，
//          具体做法是：
//            启动定时任务检查数据上限，如果超过了数量上限，sorted-set正序排序，删除一定数量的元素，同时删除hash表中的对应元素；
//                                    若没超过上限，还是sorted-set正序排序，然后检查一定量的数据是否过期(根据业务的过期时间)，若过期了，删除这批次元素，并删除hash表中对应的元素。
//
//          综上来说，用string存储有过期时间的用户信息比较简单，因为有redis来做这个删除和逐出策略，就是会造成redis中的key的数量很多。
//          用hash表存储，比较麻烦，需要一个hash表，一个sorted-set，和一个定时任务模仿redis的删除和逐出策略，但redis中的key的数量就不会很多。
//          具体还是很具业务来选择吧！
//
//     */
//    private void testLoginCookies(Jedis conn) throws InterruptedException {
//        System.out.println("\n----- testLoginCookies -----");
//        String token = UUID.randomUUID().toString();
//        //更新token的相关信息
//        updateToken(conn, token, "userInfo", "商品a");
//
//        //根据token从hash表中获取用户信息
//        String r = checkToken(conn, token);
//        System.out.println(r);
//        assert r != null;
//
//        System.out.println("启动一个定时任务，定期清理哈希表和有序集合中的过期的token数据");
//
//        //定时任务（先根据有序集合中分数删除旧的token数据，再根据删除的token删除哈希表中对应数据）
//        CleanSessionsThread thread = new CleanSessionsThread(0);
//        thread.start();
//        Thread.sleep(1000);
//        thread.quit();
//        Thread.sleep(2000);
//        if (thread.isAlive()){
//            throw new RuntimeException("The clean sessions thread is still alive?!?");
//        }
//
//        //查看哈希表中token的数量
//        long s = conn.hlen("login:");
//        System.out.println("The current number of sessions still available is: " + s);
//        assert s == 0;
//    }
//
//    /*
//     用redis保存用户购物车：
//        1、文中做法是：用hash来存储，token为key，商品id为field，商品数量为value。
//     因为token是随机可变的，作为key时，只适用于临时(时间取决于token的有效期限，定时任务会删除无效的token)的购物车。
//        2、要将购物车与用户一一绑定，就要使用user的不变且唯一的属性，例如主键id，cart:userId，这样该购物车就变成永久的了。
//
//        在redis中用hash维护一个商品池，field为商品ID，value为商品json数据，用hmget去获取
//        或者商品的详细信息可以根据ID去数据库查
//     */
//    public void testShopppingCartCookies(Jedis conn) throws InterruptedException {
//        System.out.println("\n----- testShopppingCartCookies -----");
//        String token = UUID.randomUUID().toString();
//
//        System.out.println("We'll refresh our session...");
//        updateToken(conn, token, "username", "itemX");
//        System.out.println("And add an item to the shopping cart");
//        addToCart(conn, token, "itemY", 3);
//        Map<String,String> r = conn.hgetAll("cart:" + token);
//        System.out.println("Our shopping cart currently has:");
//        for (Map.Entry<String,String> entry : r.entrySet()){
//            System.out.println("  " + entry.getKey() + ": " + entry.getValue());
//        }
//        System.out.println();
//
//        assert r.size() >= 1;
//
//        System.out.println("Let's clean out our sessions and carts");
//        CleanFullSessionsThread thread = new CleanFullSessionsThread(0);
//        thread.start();
//        Thread.sleep(1000);
//        thread.quit();
//        Thread.sleep(2000);
//        if (thread.isAlive()){
//            throw new RuntimeException("The clean sessions thread is still alive?!?");
//        }
//
//        r = conn.hgetAll("cart:" + token);
//        System.out.println("Our shopping cart now contains:");
//        for (Map.Entry<String,String> entry : r.entrySet()){
//            System.out.println("  " + entry.getKey() + ": " + entry.getValue());
//        }
//        assert r.size() == 0;
//    }
//
//    public void testCacheRows(Jedis conn)
//        throws InterruptedException
//    {
//        System.out.println("\n----- testCacheRows -----");
//        System.out.println("First, let's schedule caching of itemX every 5 seconds");
//        scheduleRowCache(conn, "itemX", 5);
//        System.out.println("Our schedule looks like:");
//        Set<Tuple> s = conn.zrangeWithScores("schedule:", 0, -1);
//        for (Tuple tuple : s){
//            System.out.println("  " + tuple.getElement() + ", " + tuple.getScore());
//        }
//        assert s.size() != 0;
//
//        System.out.println("We'll start a caching thread that will cache the data...");
//
//        CacheRowsThread thread = new CacheRowsThread();
//        thread.start();
//
//        Thread.sleep(1000);
//        System.out.println("Our cached data looks like:");
//        String r = conn.get("inv:itemX");
//        System.out.println(r);
//        assert r != null;
//        System.out.println();
//
//        System.out.println("We'll check again in 5 seconds...");
//        Thread.sleep(5000);
//        System.out.println("Notice that the data has changed...");
//        String r2 = conn.get("inv:itemX");
//        System.out.println(r2);
//        System.out.println();
//        assert r2 != null;
//        assert !r.equals(r2);
//
//        System.out.println("Let's force un-caching");
//        scheduleRowCache(conn, "itemX", -1);
//        Thread.sleep(1000);
//        r = conn.get("inv:itemX");
//        System.out.println("The cache was cleared? " + (r == null));
//        assert r == null;
//
//        thread.quit();
//        Thread.sleep(2000);
//        if (thread.isAlive()){
//            throw new RuntimeException("The database caching thread is still alive?!?");
//        }
//    }
//
//    public void testCacheRequest(Jedis conn) {
//        System.out.println("\n----- testCacheRequest -----");
//        String token = UUID.randomUUID().toString();
//
//        Callback callback = new Callback(){
//            public String call(String request){
//                return "content for " + request;
//            }
//        };
//
//        updateToken(conn, token, "username", "itemX");
//        String url = "http://test.com/?item=itemX";
//        System.out.println("We are going to cache a simple request against " + url);
//        String result = cacheRequest(conn, url, callback);
//        System.out.println("We got initial content:\n" + result);
//        System.out.println();
//
//        assert result != null;
//
//        System.out.println("To test that we've cached the request, we'll pass a bad callback");
//        String result2 = cacheRequest(conn, url, null);
//        System.out.println("We ended up getting the same response!\n" + result2);
//
//        assert result.equals(result2);
//
//        assert !canCache(conn, "http://test.com/");
//        assert !canCache(conn, "http://test.com/?item=itemX&_=1234536");
//    }
//
//    public String checkToken(Jedis conn, String token) {
//        return conn.hget("login:", token);
//    }
//
//    public void updateToken(Jedis conn, String token, String user, String item) {
//        long timestamp = System.currentTimeMillis() / 1000;
//        conn.hset("login:", token, user);
//        conn.zadd("recent:", timestamp, token);
//        /*
//        记录用户最近浏览的商品：一个用户一个有序集合，member为商品ID，score为时间戳（因为zset添加重复元素时，后者会覆盖前者。这样是按最近浏览进行排序且不会重复，若允许重复就用列表）
//        记录用户浏览次数最多的商品排行(猜你喜欢功能)
//            方式一：用有序集合：一个用户一个有序集合，member为商品ID，score为浏览次数，用zincrby对score进行加减。
//            方式二：用hash：一个用户一个哈希表，field为商品ID，value为浏览次数。用hincrby对score进行加减。
//            方式三：string会造成redis中的key太多，不好。
//         */
//        if (item != null) {
//            conn.zadd("viewed:" + token, timestamp, item);
//            conn.zremrangeByRank("viewed:" + token, 0, -26);
//            conn.zincrby("viewed:", -1, item);
//        }
//    }
//
//    public void addToCart(Jedis conn, String session, String item, int count) {
//        if (count <= 0) {
//            conn.hdel("cart:" + session, item);
//        } else {
//            conn.hset("cart:" + session, item, String.valueOf(count));
//        }
//    }
//
//    public void scheduleRowCache(Jedis conn, String rowId, int delay) {
//        conn.zadd("delay:", delay, rowId);
//        conn.zadd("schedule:", System.currentTimeMillis() / 1000, rowId);
//    }
//
//    public String cacheRequest(Jedis conn, String request, Callback callback) {
//        if (!canCache(conn, request)){
//            return callback != null ? callback.call(request) : null;
//        }
//
//        String pageKey = "cache:" + hashRequest(request);
//        String content = conn.get(pageKey);
//
//        if (content == null && callback != null){
//            content = callback.call(request);
//            conn.setex(pageKey, 300, content);
//        }
//
//        return content;
//    }
//
//    public boolean canCache(Jedis conn, String request) {
//        try {
//            URL url = new URL(request);
//            HashMap<String,String> params = new HashMap<String,String>();
//            if (url.getQuery() != null){
//                for (String param : url.getQuery().split("&")){
//                    String[] pair = param.split("=", 2);
//                    params.put(pair[0], pair.length == 2 ? pair[1] : null);
//                }
//            }
//
//            String itemId = extractItemId(params);
//            if (itemId == null || isDynamic(params)) {
//                return false;
//            }
//            Long rank = conn.zrank("viewed:", itemId);
//            return rank != null && rank < 10000;
//        }catch(MalformedURLException mue){
//            return false;
//        }
//    }
//
//    public boolean isDynamic(Map<String,String> params) {
//        return params.containsKey("_");
//    }
//
//    public String extractItemId(Map<String,String> params) {
//        return params.get("item");
//    }
//
//    public String hashRequest(String request) {
//        return String.valueOf(request.hashCode());
//    }
//
//    public interface Callback {
//        public String call(String request);
//    }
//
//    public class CleanSessionsThread extends Thread {
//        private Jedis conn;
//        private int limit;
//        private boolean quit;
//
//        public CleanSessionsThread(int limit) {
//            this.conn = new Jedis("localhost");
//            this.conn.select(15);
//            this.limit = limit;
//        }
//
//        public void quit() {
//            quit = true;
//        }
//
//        public void run() {
//            while (!quit) {
//                long size = conn.zcard("recent:");
//                if (size <= limit){
//                    try {
//                        sleep(1000);
//                    }catch(InterruptedException ie){
//                        Thread.currentThread().interrupt();
//                    }
//                    continue;
//                }
//
//                long endIndex = Math.min(size - limit, 100);
//                Set<String> tokenSet = conn.zrange("recent:", 0, endIndex - 1);
//                String[] tokens = tokenSet.toArray(new String[tokenSet.size()]);
//
//                ArrayList<String> sessionKeys = new ArrayList<String>();
//                for (String token : tokens) {
//                    sessionKeys.add("viewed:" + token);
//                }
//
//                //删除redis中的多个token键值对
//                conn.del(sessionKeys.toArray(new String[sessionKeys.size()]));
//                //删除哈希表中的多个token键值对
//                conn.hdel("login:", tokens);
//                //删除有序集合中的多个token
//                conn.zrem("recent:", tokens);
//            }
//        }
//    }
//
//    public class CleanFullSessionsThread
//        extends Thread
//    {
//        private Jedis conn;
//        private int limit;
//        private boolean quit;
//
//        public CleanFullSessionsThread(int limit) {
//            this.conn = new Jedis("localhost");
//            this.conn.select(15);
//            this.limit = limit;
//        }
//
//        public void quit() {
//            quit = true;
//        }
//
//        public void run() {
//            while (!quit) {
//                long size = conn.zcard("recent:");
//                if (size <= limit){
//                    try {
//                        sleep(1000);
//                    }catch(InterruptedException ie){
//                        Thread.currentThread().interrupt();
//                    }
//                    continue;
//                }
//
//                long endIndex = Math.min(size - limit, 100);
//                Set<String> sessionSet = conn.zrange("recent:", 0, endIndex - 1);
//                String[] sessions = sessionSet.toArray(new String[sessionSet.size()]);
//
//                ArrayList<String> sessionKeys = new ArrayList<String>();
//                for (String sess : sessions) {
//                    sessionKeys.add("viewed:" + sess);
//                    sessionKeys.add("cart:" + sess);
//                }
//
//                conn.del(sessionKeys.toArray(new String[sessionKeys.size()]));
//                conn.hdel("login:", sessions);
//                conn.zrem("recent:", sessions);
//            }
//        }
//    }
//
//    public class CacheRowsThread
//        extends Thread
//    {
//        private Jedis conn;
//        private boolean quit;
//
//        public CacheRowsThread() {
//            this.conn = new Jedis("localhost");
//            this.conn.select(15);
//        }
//
//        public void quit() {
//            quit = true;
//        }
//
//        public void run() {
//            Gson gson = new Gson();
//            while (!quit){
//                Set<Tuple> range = conn.zrangeWithScores("schedule:", 0, 0);
//                Tuple next = range.size() > 0 ? range.iterator().next() : null;
//                long now = System.currentTimeMillis() / 1000;
//                if (next == null || next.getScore() > now){
//                    try {
//                        sleep(50);
//                    }catch(InterruptedException ie){
//                        Thread.currentThread().interrupt();
//                    }
//                    continue;
//                }
//
//                String rowId = next.getElement();
//                double delay = conn.zscore("delay:", rowId);
//                if (delay <= 0) {
//                    conn.zrem("delay:", rowId);
//                    conn.zrem("schedule:", rowId);
//                    conn.del("inv:" + rowId);
//                    continue;
//                }
//
//                Inventory row = Inventory.get(rowId);
//                conn.zadd("schedule:", now + delay, rowId);
//                conn.set("inv:" + rowId, gson.toJson(row));
//            }
//        }
//    }
//
//    public static class Inventory {
//        private String id;
//        private String data;
//        private long time;
//
//        private Inventory (String id) {
//            this.id = id;
//            this.data = "data to cache...";
//            this.time = System.currentTimeMillis() / 1000;
//        }
//
//        public static Inventory get(String id) {
//            return new Inventory(id);
//        }
//    }
//}
